using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.SharePoint;
using Sapphire.Linq.FieldMaps;
using Sapphire.Linq.Framework;

namespace Sapphire.Linq.Mappers
{
    public class SPListItemMapper : ObjectMapper<SPListItem>
    {
        private class CustomMapsDescription
        {
            public PropertyDescriptor PropertyDescriptor { get; set; }
            public SPField Field { get; set; }
        }

        readonly Dictionary<Type, IEnumerable<CustomMapsDescription>> _properties = new Dictionary<Type, IEnumerable<CustomMapsDescription>>();

        protected override TTo Map<TTo>(SPListItem listItem, object[] parameters)
        {
            var entity = Activator.CreateInstance<TTo>();

            IPropertyMapsContext mapsContext = parameters.OfType<ICustomPropertyMaps>().First().MapsContext;

            var properties = GetCustomMapsDescriptions<TTo>(mapsContext, listItem);

            foreach (var description in properties)
            {
                var propertyValue = GetPropertyValue(listItem, description.Field, description.PropertyDescriptor, parameters);
                description.PropertyDescriptor.SetValue(entity, propertyValue);
            }

            return entity;
        }

        private IEnumerable<CustomMapsDescription> GetCustomMapsDescriptions<T>(IPropertyMapsContext mapsContext, SPListItem listItem)
        {
            var type = typeof(T);
            if (!_properties.ContainsKey(type))
            {
                IEnumerable<CustomMapsDescription> descriptions =
                    TypeDescriptor.GetProperties(type)
                        .Cast<PropertyDescriptor>()
                        .Select(p => CreateCustomMapsDescription<T>(mapsContext, listItem, p))
                        .Where(x => x != null)
                        .ToList();

                _properties[type] = descriptions;
            }
            return _properties[type];
        }

        private static CustomMapsDescription CreateCustomMapsDescription<T>(IPropertyMapsContext mapsContext, SPListItem listItem, PropertyDescriptor propertyDescriptor)
        {
            var field = mapsContext.GetField<T>(listItem.ParentList, propertyDescriptor.Name);

            if (field == null)
                return null;

            return new CustomMapsDescription
                       {
                           PropertyDescriptor = propertyDescriptor,
                           Field = field
                       };
        }


        private object GetPropertyValue(SPListItem item, SPField field, PropertyDescriptor propertyDescriptor, params  object[] parameters)
        {
            object fieldValue;

            try
            {
                fieldValue = item[field.Id];
            }
            catch (ArgumentException)
            {
                throw new ArgumentException("Invalid custom property map for " + propertyDescriptor.Name, "field");
            }


            if (fieldValue == null)
                return null;

            if (propertyDescriptor.PropertyType.IsAssignableFrom(fieldValue.GetType()))
                return fieldValue;

            return MapTo(propertyDescriptor, fieldValue, parameters, field);
        }

        private readonly Dictionary<Type, Func<object, object[], object>> _mapToLambdas = new Dictionary<Type, Func<object, object[], object>>();

        private object MapTo(PropertyDescriptor propertyDescriptor, object fieldValue, object[] parameters, SPField field)
        {
            if (!_mapToLambdas.ContainsKey(propertyDescriptor.PropertyType))
            {
                var valueExpr = Expression.Parameter(typeof(object), "v");
                var parametersExpr = Expression.Parameter(typeof(object[]), "ps");

                var call = Expression.Call(
                                Expression.Constant(Mapper.Instance),
                                "Map",
                                new[] { propertyDescriptor.PropertyType },
                                valueExpr, parametersExpr
                    );

                var lambda = Expression.Lambda<Func<object, object[], object>>(call, valueExpr, parametersExpr);
                _mapToLambdas.Add(propertyDescriptor.PropertyType, lambda.Compile());
            }

            return _mapToLambdas[propertyDescriptor.PropertyType](fieldValue, parameters.Include(field).ToArray());
        }
    }
}